import pandas as pd
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
from matplotlib.widgets import Button
import time
# 导入tree模块以访问底层功能
from sklearn import tree

# 自定义函数：以树形结构在终端显示决策树
def print_tree_structure(model, feature_names, class_names):
    """以树形结构打印决策树，根节点在上，子节点向下延伸"""
    # 获取树的结构
    tree_ = model.tree_
    
    def recurse(node, depth, prefix="", is_left=True, is_last=True):
        # 构建连接线和前缀
        if depth == 0:
            # 根节点
            print("┌─── 决策树根节点 ───┐")
        else:
            # 构建连接线
            connector = "└───" if is_last else "├───"
            print(f"{prefix}{connector}", end="")
        
        # 检查是否是叶节点
        if tree_.children_left[node] == -1 and tree_.children_right[node] == -1:
            # 叶节点：显示类别
            value = tree_.value[node][0]
            class_idx = np.argmax(value)
            count = int(value[class_idx])
            total = int(np.sum(value))
            class_name = class_names[class_idx]
            print(f" [叶节点] 类别: {class_name} ({count}/{total})")
        else:
            # 内部节点：显示分裂条件
            feature_idx = tree_.feature[node]
            feature_name = feature_names[feature_idx]
            threshold = tree_.threshold[node]
            print(f" {feature_name} ≤ {threshold:.2f}")
            
            # 递归打印子节点
            new_prefix = prefix
            if depth > 0:
                new_prefix += "    " if is_last else "│   "
            
            # 打印左子树
            recurse(tree_.children_left[node], depth + 1, new_prefix, True, False)
            # 打印右子树
            recurse(tree_.children_right[node], depth + 1, new_prefix, False, True)
    
    # 开始递归打印
    recurse(0, 0)

# 设置Matplotlib支持中文显示
# 使用Windows系统默认的中文字体，移除不必要的字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']  # 简化字体配置
plt.rcParams['axes.unicode_minus'] = False  # 正确显示负号
plt.rcParams['font.family'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']  # 设置字体族

# 1. Prepare Dataset
def create_fruit_dataset():
    """创建水果数据集（4种水果，共30个样本）"""
    data = {
        '颜色': [
            # 苹果样本 - 8个
            '红色', '红色', '红色', '红色', '红色', '红色', '红色', '红色',
            # 香蕉样本 - 8个
            '黄色', '黄色', '黄色', '黄色', '黄色', '黄色', '黄色', '黄色',
            # 柠檬样本 - 7个
            '黄色', '黄色', '黄色', '黄色', '黄色', '黄色', '黄色',
            # 草莓样本 - 7个
            '红色', '红色', '红色', '红色', '红色', '红色', '红色'
        ],
        '形状': [
            # 苹果样本
            '圆形', '圆形', '圆形', '圆形', '圆形', '圆形', '圆形', '圆形',
            # 香蕉样本
            '长形', '长形', '长形', '长形', '长形', '长形', '长形', '长形',
            # 柠檬样本
            '圆形', '圆形', '圆形', '圆形', '圆形', '椭圆形', '椭圆形',
            # 草莓样本
            '心形', '心形', '心形', '心形', '心形', '心形', '心形'
        ],
        '大小': [
            # 苹果样本
            '中等', '中等', '中等', '中等', '大型', '大型', '小型', '小型',
            # 香蕉样本
            '中等', '中等', '中等', '中等', '大型', '大型', '小型', '小型',
            # 柠檬样本
            '小型', '小型', '小型', '小型', '小型', '小型', '小型',
            # 草莓样本
            '小型', '小型', '小型', '小型', '小型', '小型', '小型'
        ],
        '重量': [
            # 苹果样本
            '中等', '中等', '重', '重', '重', '重', '轻', '轻',
            # 香蕉样本
            '中等', '中等', '中等', '中等', '重', '重', '轻', '轻',
            # 柠檬样本
            '轻', '轻', '轻', '轻', '轻', '轻', '轻',
            # 草莓样本
            '轻', '轻', '轻', '轻', '轻', '轻', '轻'
        ],
        '类型': [
            # 苹果样本
            '苹果', '苹果', '苹果', '苹果', '苹果', '苹果', '苹果', '苹果',
            # 香蕉样本
            '香蕉', '香蕉', '香蕉', '香蕉', '香蕉', '香蕉', '香蕉', '香蕉',
            # 柠檬样本
            '柠檬', '柠檬', '柠檬', '柠檬', '柠檬', '柠檬', '柠檬',
            # 草莓样本
            '草莓', '草莓', '草莓', '草莓', '草莓', '草莓', '草莓'
        ]
    }
    return pd.DataFrame(data)

# 2. Data Preprocessing
def preprocess_data(df):
    """数据预处理：编码分类特征"""
    label_encoders = {}
    df_encoded = df.copy()
    
    # 编码每个分类特征
    for column in ['颜色', '形状', '大小', '重量', '类型']:
        le = LabelEncoder()
        df_encoded[column] = le.fit_transform(df[column])
        label_encoders[column] = le
    
    return df_encoded, label_encoders

# 3. Train Decision Tree Model
def train_model(X_train, y_train, max_depth=5):
    """训练决策树模型"""
    model = DecisionTreeClassifier(
        criterion='entropy',  # 使用信息增益
        max_depth=max_depth,  # 限制树的深度
        random_state=42,
        min_samples_split=2  # 分裂至少需要2个样本
    )
    model.fit(X_train, y_train)
    return model

# 4. Interactive Visualization with Start Button
def visualize_training_with_button(X_train, y_train, X_test, y_test, feature_names, class_names, label_encoders, df):
    """使用开始按钮的决策树训练过程交互式可视化，专注于完整展示训练过程"""
    print("\n交互式决策树训练可视化已加载。")
    print("点击'开始训练'按钮开始逐步可视化过程。")
    
    # 创建极大图形，为决策树标签显示提供充足空间
    fig = plt.figure(figsize=(80, 25))  # 大幅增加图形宽度和高度
    
    # 创建轴 - 完全重新设计布局，为决策树分配最大可能空间
    ax_button = plt.axes([0.47, 0.98, 0.06, 0.02])  # 按钮最小化
    # 决策树占据左侧几乎全部空间
    ax_tree = plt.axes([0.01, 0.01, 0.9, 0.96])  # 决策树区域宽度增大到0.9
    ax_steps = plt.axes([0.92, 0.01, 0.07, 0.96])  # 训练步骤区域缩小到最小
    
    # 添加开始按钮
    start_button = Button(ax_button, '开始训练')
    
    # 跟踪训练进度的变量
    current_depth = 0
    max_depth = min(6, len(X_train) // 2)  # 限制树的最大深度
    training_completed = False
    models = []  # 存储每个深度的模型
    
    # 初始化显示，专注于训练过程
    def initialize_display():
        # 清除树区域并显示占位符文本
        ax_tree.clear()
        ax_tree.text(0.5, 0.5, '点击开始训练按钮开始', 
                    horizontalalignment='center',
                    verticalalignment='center',
                    fontsize=14)
        ax_tree.axis('off')
        
        # 初始化训练步骤显示区域 - 提供更多初始信息
        ax_steps.axis('off')
        steps_content = '训练步骤详细记录:\n\n'
        steps_content += '准备开始训练...\n\n'
        steps_content += '训练流程说明:\n'
        steps_content += '1. 数据准备：分割训练集和测试集\n'
        steps_content += '2. 逐步训练：从深度1到最大深度\n'
        steps_content += '3. 评估模型：计算各深度的准确率\n'
        steps_content += '4. 选择最佳：找出最优决策树模型\n\n'
        steps_content += '点击上方"开始训练"按钮启动训练过程。'
        
        ax_steps.text(0.02, 0.98, steps_content, 
                     verticalalignment='top', 
                     fontsize=10)
        ax_steps.set_title('决策树训练过程详细信息', fontsize=14, pad=10)
        
        # 使用subplots_adjust代替tight_layout以更好地控制布局
        plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.95, wspace=0.02, hspace=0.02)
    
    # 更新特定深度的显示函数
    def update_display(depth):
        # 清除树子图
        ax_tree.clear()
        
        # 训练当前深度的模型
        model = DecisionTreeClassifier(
            criterion='entropy',
            max_depth=depth,
            random_state=42,
            min_samples_split=2
        )
        
        # 记录开始训练时间
        start_time = time.time()
        model.fit(X_train, y_train)
        training_time = time.time() - start_time
        models.append(model)
        
        # 评估模型
        y_pred_train = model.predict(X_train)
        y_pred_test = model.predict(X_test)
        train_accuracy = accuracy_score(y_train, y_pred_train)
        test_accuracy = accuracy_score(y_test, y_pred_test)
        
        # 获取模型信息
        node_count = model.tree_.node_count
        leaf_count = model.tree_.n_leaves
        
        # 计算特征重要性（如果模型支持）
        feature_importance = []
        if hasattr(model, 'feature_importances_'):
            importance = model.feature_importances_
            for i, imp in enumerate(importance):
                if imp > 0:
                    feature_importance.append((feature_names[i], imp))
            # 按重要性排序
            feature_importance.sort(key=lambda x: x[1], reverse=True)
        
        # 直接在控制台打印详细的训练信息
        print(f"\n{"="*50}")
        print(f"步骤 {depth}/{max_depth}: 训练深度为 {depth} 的决策树")
        print(f"{"="*50}")
        
        # 时间信息
        print("[时间信息]")
        print(f"  - 开始训练时间: {time.strftime("%H:%M:%S")}")
        print(f"  - 训练完成时间: {time.strftime("%H:%M:%S")}")
        print(f"  - 训练耗时: {training_time:.4f} 秒")
        
        # 模型结构信息
        print("\n[模型结构]")
        print(f"  - 决策树深度: {depth}")
        print(f"  - 总节点数: {node_count}")
        print(f"  - 叶节点数: {leaf_count}")
        print(f"  - 分裂节点数: {node_count - leaf_count}")
        
        # 模型评估
        print("\n[模型评估]")
        print(f"  - 训练集准确率: {train_accuracy:.4f} ({train_accuracy*100:.1f}%)")
        print(f"  - 测试集准确率: {test_accuracy:.4f} ({test_accuracy*100:.1f}%)")
        
        # 计算过拟合指标
        overfitting_diff = train_accuracy - test_accuracy
        print(f"  - 过拟合程度: {overfitting_diff:.4f}")
        if overfitting_diff > 0.1:
            print(f"  - 警告: 当前模型可能存在过拟合")
        elif overfitting_diff < -0.1:
            print(f"  - 警告: 当前模型可能存在欠拟合")
        else:
            print(f"  - 模型拟合状态良好")
        
        # 特征重要性
        if feature_importance:
            print("\n[特征重要性排名]")
            for i, (feature, imp) in enumerate(feature_importance[:3], 1):  # 显示前3个最重要的特征
                print(f"  {i}. {feature}: {imp:.4f} ({imp*100:.1f}%)")
        
        # 预测示例
        sample_indices = [0, min(1, len(X_test)-1)]  # 取前两个测试样本作为示例
        print("\n[预测示例]")
        for idx in sample_indices:
            if idx < len(X_test):
                pred = model.predict([X_test.iloc[idx]])[0]
                true = y_test.iloc[idx]
                print(f"  - 样本 {idx+1}: 预测={class_names[pred]} | 实际={class_names[true]}", end="")
                if pred == true:
                    print(" ✓")
                else:
                    print(" ✗")
        
        # 在界面上保持训练进行中的提示
        ax_steps.clear()
        ax_steps.axis('off')
        ax_steps.text(0.5, 0.5, f'训练进行中...\n当前深度: {depth}/{max_depth}\n详情请查看控制台输出',
                     verticalalignment='center', 
                     horizontalalignment='center',
                     fontsize=10, 
                     bbox=dict(boxstyle="round,pad=1", facecolor="lightyellow", alpha=0.7))
        
        # 尝试使用最简化的参数设置来显示决策树
        ax_tree.clear()
        
        # 最小化参数，只保留必要的显示设置
        # 增大字体大小，确保节点中的文字清晰可见
        plot_tree(
            model,
            ax=ax_tree,
            feature_names=feature_names,
            class_names=class_names,
            label='all',  # 理论上应该强制显示所有分支标签
            proportion=False,  # 允许树完全展开
            fontsize=12  # 显著增大字体大小，确保清晰可见
        )
        
        # 优化右侧区域，专门用于显示完整的决策树文本表示
        # 这可以确保用户能看到所有层级的判断条件
        tree_text = tree.export_text(
            model,
            feature_names=feature_names,
            spacing=2,  # 紧凑间距以显示更多内容
            decimals=2
        )
        
        # 在右侧区域显示完整的文本表示决策树
        # 这是确保用户能看到所有层级判断条件的可靠方法
        ax_steps.clear()
        ax_steps.text(0.01, 0.99, "决策树完整结构:\n\n" + tree_text, 
                    fontsize=8, 
                    family='monospace',
                    verticalalignment='top',
                    transform=ax_steps.transAxes,
                    wrap=True)
        ax_steps.axis('off')
        ax_steps.set_title(f'决策树文本表示（深度: {depth}）', fontsize=10)
        
        # 添加说明文本，告知用户如何查看所有层级的判断条件
        ax_steps.text(0.01, 0.01, "注意：图形化决策树可能仅在前两层显示连线标签\n请参考右侧文本表示查看所有层级的判断条件",
                    fontsize=7,
                    color='red',
                    verticalalignment='bottom',
                    transform=ax_steps.transAxes)
        
        # 确保图形足够大以显示所有内容
        fig.set_size_inches(60, 20)
        # 移除tight_layout，因为它可能会压缩布局并隐藏标签
        # 手动调整布局边距以确保有足够空间显示所有标签
        plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.95, wspace=0.02, hspace=0.02)
        
        ax_tree.set_title(f'决策树（深度: {depth}）\n' +
                        f'训练准确率: {train_accuracy:.2f} | 测试准确率: {test_accuracy:.2f}', 
                        fontsize=12)
        
        # 在控制台打印训练过程
        print(f"\n训练步骤 {depth}/{max_depth}:")
        print(f"  树深度: {depth}")
        print(f"  训练集准确率: {train_accuracy:.2f}")
        print(f"  测试集准确率: {test_accuracy:.2f}")
        print(f"  总节点数: {node_count}")
        print(f"  叶节点数: {leaf_count}")
        
        fig.canvas.draw_idle()
        
        # 不再返回steps_content，因为训练过程现在只在控制台显示
        return ""  # 返回空字符串作为占位符
    
    # 按钮点击回调函数
    def on_start_clicked(event):
        nonlocal current_depth, training_completed
        
        if training_completed:
            print("训练已经完成。请关闭窗口退出。")
            return
            
        # 训练期间禁用按钮
        start_button.label.set_text('训练进行中...')
        start_button.color = 'lightgray'
        start_button.hovercolor = 'gray'
        fig.canvas.draw_idle()
        
        # 在控制台显示详细的数据准备信息
        print(f"\n{"="*60}")
        print("第一阶段: 数据准备与分析")
        print(f"{"="*60}")
        
        # 数据集基本信息
        print("[数据集信息]")
        print(f"  - 总样本数: {len(df)}")
        print(f"  - 训练集大小: {len(X_train)} 样本 ({len(X_train)/len(df)*100:.1f}%)")
        print(f"  - 测试集大小: {len(X_test)} 样本 ({len(X_test)/len(df)*100:.1f}%)")
        print(f"  - 特征数量: {len(feature_names)}")
        print(f"  - 类别数量: {len(class_names)}")
        
        # 特征信息
        print("\n[特征信息]")
        for i, feature in enumerate(feature_names, 1):
            unique_values = df[feature].unique()
            print(f"  {i}. {feature}: {len(unique_values)}个不同值")
            print(f"     [{"、".join(map(str, unique_values[:3]))}")
            if len(unique_values) > 3:
                print(f"      ... 等{len(unique_values)}个]")
            else:
                print("     ]")
        
        # 类别分布
        print("\n[类别分布]")
        class_counts = df['类型'].value_counts()
        for cls, count in class_counts.items():
            percentage = count / len(df) * 100
            print(f"  - {cls}: {count}个样本 ({percentage:.1f}%)")
        
        print("\n准备开始逐步训练不同深度的决策树...")
        
        # 在图形界面显示简单的训练进行中提示
        ax_tree.clear()
        ax_tree.text(0.5, 0.5, '训练进行中...\n详情请查看控制台输出', 
                    horizontalalignment='center',
                    verticalalignment='center',
                    fontsize=16)
        ax_tree.axis('off')
        fig.canvas.draw_idle()
        plt.pause(1)
        
        # 只在控制台打印训练准备信息
        print(f"\n{'-'*60}")
        print(f"数据准备完成。训练集: {len(X_train)}样本, 测试集: {len(X_test)}样本")
        print(f"{'-'*60}")
        print("第二阶段: 逐步训练不同深度的决策树")
        print(f"{'-'*60}")
        print(f"将从深度1到深度{max_depth}逐步训练并评估...")
        
        # 在界面显示训练进行中的提示
        ax_steps.clear()
        ax_steps.axis('off')
        ax_steps.text(0.5, 0.5, '训练进行中...\n详情请查看控制台输出',
                     verticalalignment='center', 
                     horizontalalignment='center',
                     fontsize=10, 
                     bbox=dict(boxstyle="round,pad=1", facecolor="lightyellow", alpha=0.7))
        fig.canvas.draw_idle()
        plt.pause(1)
        
        # 运行训练步骤，添加延迟
        for depth in range(1, max_depth + 1):
            # 直接更新模型，不需要传递steps_content
            update_display(depth)
            plt.pause(2.5)  # 步骤之间的延迟略微增加，让用户有足够时间查看
            current_depth = depth
        
        # 找出最佳模型
        best_depth = 1
        best_acc = 0
        best_models = []
        
        for i, model in enumerate(models):
            depth = i + 1
            test_acc = accuracy_score(y_test, model.predict(X_test))
            best_models.append((depth, test_acc))
            if test_acc > best_acc:
                best_acc = test_acc
                best_depth = depth
        
        # 计算平均指标
        avg_training_time = sum(model.tree_.node_count * 0.001 for model in models) / len(models) if models else 0
        
        # 初始化steps_content变量，避免UnboundLocalError
        steps_content = ""
        
        # 更新步骤显示 - 最终结果，更详细的总结
        steps_content += '\n\n' + '='*60
        steps_content += '\n第三阶段: 训练完成总结与模型选择'
        steps_content += '\n' + '='*60
        
        steps_content += '\n[最佳模型信息]'
        steps_content += f'\n  - 最佳决策树深度: {best_depth}'
        steps_content += f'\n  - 最佳测试准确率: {best_acc:.4f} ({best_acc*100:.1f}%)'
        
        # 获取最佳模型的详细信息
        best_model = models[best_depth-1]
        steps_content += f'\n  - 最佳模型节点数: {best_model.tree_.node_count}'
        steps_content += f'\n  - 最佳模型叶节点数: {best_model.tree_.n_leaves}'
        
        # 特征重要性（最佳模型）
        if hasattr(best_model, 'feature_importances_'):
            importance = best_model.feature_importances_
            top_features = sorted(zip(feature_names, importance), key=lambda x: x[1], reverse=True)
            steps_content += '\n\n[最佳模型特征重要性]'
            for i, (feature, imp) in enumerate(top_features[:5], 1):  # 显示前5个
                steps_content += f'\n  {i}. {feature}: {imp:.4f} ({imp*100:.1f}%)'
        
        # 所有深度性能对比
        steps_content += '\n\n[各深度模型性能完整对比]'
        steps_content += '\n  深度 | 测试准确率 | 过拟合程度'
        steps_content += '\n  ' + '-'*30
        for depth, acc in best_models:
            train_acc = accuracy_score(y_train, models[depth-1].predict(X_train))
            overfit = train_acc - acc
            steps_content += f"\n  {depth:4d} | {acc:.4f}    | {overfit:8.4f}"
        
        # 训练总结
        steps_content += '\n\n[训练过程总结]'
        steps_content += f'\n  - 总训练深度数: {max_depth}'
        steps_content += f'\n  - 总训练样本数: {len(X_train) + len(X_test)}'
        steps_content += f'\n  - 平均训练时间: {avg_training_time:.2f} 秒'
        steps_content += '\n\n训练完成！您可以查看左侧的最终决策树可视化。'
        
        # 更新步骤显示区域，使用两列布局
        ax_steps.clear()
        ax_steps.axis('off')
        
        # 将内容分成两列
        content_lines = steps_content.split('\n')
        middle_line = len(content_lines) // 2
        
        left_content = '\n'.join(content_lines[:middle_line])
        right_content = '\n'.join(content_lines[middle_line:])
        
        ax_steps.text(0.01, 0.99, left_content, 
                     verticalalignment='top', 
                     horizontalalignment='left',
                     fontsize=8)
        
        ax_steps.text(0.51, 0.99, right_content, 
                     verticalalignment='top', 
                     horizontalalignment='left',
                     fontsize=8)
        ax_steps.set_title('决策树训练过程详细信息 - 完成', fontsize=14, pad=10)
        fig.canvas.draw_idle()
        
        # 训练完成后更新按钮
        start_button.label.set_text('训练已完成')
        training_completed = True
        print("\n训练过程完成！")
        print(f"最佳决策树深度: {best_depth}, 准确率: {best_acc:.4f}")
    
    # Connect button to callback
    start_button.on_clicked(on_start_clicked)
    
    # Initialize the display
    initialize_display()
    
    plt.show()
    
    # Return the final model
    if models:
        return models[-1]  # Return the last (deepest) model
    else:
        # Fallback if no models were trained
        final_model = DecisionTreeClassifier(
            criterion='entropy',
            max_depth=max_depth,
            random_state=42,
            min_samples_split=2
        )
        final_model.fit(X_train, y_train)
        return final_model

# 5. Predict and Explain Decision Process
def explain_decision_tree(model, feature_names, class_names, depth):
    """阐述决策树的结构和特征重要性"""
    tree_ = model.tree_
    
    print(f"\n决策树阐述（深度：{depth}）：")
    print("-"*60)
    
    # 计算节点数量
    n_nodes = tree_.node_count
    n_leaves = 0
    
    for i in range(n_nodes):
        if tree_.children_left[i] == -1 and tree_.children_right[i] == -1:
            n_leaves += 1
    
    print(f"1. 决策树统计信息：")
    print(f"   - 总节点数: {n_nodes}")
    print(f"   - 叶节点数: {n_leaves}")
    print(f"   - 内部节点数: {n_nodes - n_leaves}")
    print(f"   - 平均深度: {depth}")
    
    # 分析特征重要性
    importances = model.feature_importances_
    importance_dict = {feature: importance for feature, importance in zip(feature_names, importances)}
    sorted_importances = sorted(importance_dict.items(), key=lambda x: x[1], reverse=True)
    
    print(f"\n2. 特征重要性分析：")
    if any(imp > 0 for _, imp in sorted_importances):
        for i, (feature, importance) in enumerate(sorted_importances, 1):
            if importance > 0:
                print(f"   - {feature}: {importance:.4f}" + (" (最重要特征)" if i == 1 else ""))
    else:
        print("   - 所有特征重要性为0（可能是单叶树）")
    
    # 分析根节点
    if n_nodes > 0 and tree_.children_left[0] != -1:
        root_feature = feature_names[tree_.feature[0]]
        root_threshold = tree_.threshold[0]
        print(f"\n3. 根节点分析：")
        print(f"   - 根节点使用特征: {root_feature}")
        print(f"   - 分裂阈值: {root_threshold:.2f}")
        print(f"   - 这表明 {root_feature} 是区分样本的最重要特征")
    
    # 分析叶节点类别分布
    print(f"\n4. 叶节点类别分布：")
    leaf_classes = []
    for i in range(n_nodes):
        if tree_.children_left[i] == -1 and tree_.children_right[i] == -1:
            value = tree_.value[i][0]
            class_idx = np.argmax(value)
            count = int(value[class_idx])
            total = int(np.sum(value))
            class_name = class_names[class_idx]
            leaf_classes.append((class_name, count, total))
    
    # 统计每个类别的叶节点数量
    class_counts = {}
    for class_name, count, total in leaf_classes:
        if class_name not in class_counts:
            class_counts[class_name] = 0
        class_counts[class_name] += 1
    
    for class_name, count in class_counts.items():
        print(f"   - {class_name}: {count}个叶节点")
    
    # 深度特定分析
    if depth == 1:
        print(f"\n5. 模型分析：")
        print(f"   - 这是一个简单的决策树桩，仅使用一个特征进行分类")
        print(f"   - 虽然结构简单，但可能已经捕捉到了最重要的区分特征")
    elif depth > 1:
        print(f"\n5. 模型分析：")
        print(f"   - 与深度较小的模型相比，当前模型能够捕获更复杂的决策边界")
        print(f"   - 随着深度增加，模型可能开始捕捉更多细节特征，但也需要注意过拟合风险")

def predict_and_explain(model, new_fruit, label_encoders, feature_names):
    """预测并解释决策过程"""
    # 编码新样本
    new_fruit_encoded = []
    for feature in feature_names:
        encoded_value = label_encoders[feature].transform([new_fruit[feature]])[0]
        new_fruit_encoded.append(encoded_value)
    
    # 进行预测
    prediction = model.predict([new_fruit_encoded])[0]
    predicted_class = label_encoders['类型'].inverse_transform([prediction])[0]
    
    # 获取决策路径
    node_indicator = model.decision_path([new_fruit_encoded])
    leaf_id = model.apply([new_fruit_encoded])[0]
    node_index = node_indicator.indices[node_indicator.indptr[0]:node_indicator.indptr[1]]
    
    # 解释决策过程
    print("\n" + "="*50)
    print("决策过程分析：")
    print("="*50)
    
    for i, node_id in enumerate(node_index):
        if node_id != leaf_id:
            feature_idx = model.tree_.feature[node_id]
            feature_name = feature_names[feature_idx]
            threshold = model.tree_.threshold[node_id]
            
            original_value = new_fruit[feature_name]
            encoded_value = new_fruit_encoded[feature_idx]
            
            if encoded_value <= threshold:
                direction = "≤"
                branch = "左子树"
            else:
                direction = ">"
                branch = "右子树"
            
            print(f"\n步骤 {i+1}:")
            print(f"  检查特征: {feature_name} = {original_value} (编码后: {encoded_value})")
            print(f"  条件: {encoded_value} {direction} {threshold:.2f}")
            print(f"  走向: {branch}")
        else:
            print(f"\n步骤 {i+1}:")
            print(f"  到达叶节点，最终预测: {predicted_class}")
    
    return predicted_class

def show_dataset_window(df):
    """显示原始数据集的窗口，包含序号列"""
    # 创建带序号的数据集副本
    df_with_index = df.copy()
    df_with_index.insert(0, '序号', range(1, len(df_with_index) + 1))
    
    # 创建更大的图形以确保完整显示
    fig, ax = plt.subplots(figsize=(14, 12))
    fig.canvas.manager.set_window_title('原始数据集')
    
    # 创建表格显示数据集
    table = ax.table(
        cellText=df_with_index.values, 
        colLabels=df_with_index.columns, 
        cellLoc='center', 
        loc='center'
    )
    table.auto_set_font_size(False)
    table.set_fontsize(10)
    table.scale(1.2, 1.8)  # 增加比例以确保更好的可见性
    
    # 美化表格
    for (i, j), cell in table.get_celld().items():
        if i == 0:  # 表头
            cell.set_text_props(weight='bold', color='white')
            cell.set_facecolor('#40466e')
        elif j == 0:  # 序号列
            cell.set_text_props(weight='bold', color='#40466e')
            cell.set_facecolor('#f0f0f0')
        cell.set_edgecolor('#000000')
    
    ax.axis('off')
    plt.tight_layout()
    return fig

def show_decision_tree_window(model, depth, feature_names, class_names):
    """显示决策树的窗口，包含节点信息解释"""
    # 创建一个包含两个子图的图形：左侧显示决策树，右侧显示解释
    fig, (ax_tree, ax_info) = plt.subplots(1, 2, figsize=(22, 14), gridspec_kw={'width_ratios': [2.5, 1.5]})
    fig.canvas.manager.set_window_title(f'决策树 - 深度 {depth}')
    
    # 绘制决策树
    plot_tree(model, feature_names=feature_names, class_names=class_names, filled=True, 
              ax=ax_tree, fontsize=8, rounded=True, precision=2)
    
    ax_tree.set_title(f'决策树分类器 - 深度 {depth}', fontsize=16, pad=20)
    
    # 在决策树下方添加水果类别名称
    class_info = "水果类别：" + "、".join(class_names)
    ax_tree.text(0.5, -0.03, class_info, 
                horizontalalignment='center', 
                verticalalignment='top', 
                transform=ax_tree.transAxes,
                fontsize=10, fontweight='bold', color='#333333')
    
    # 添加熵计算公式（使用LaTeX语法显示下标）
    entropy_formula = r"熵计算公式：H(S) = -Σ(p_i * log$_2$(p_i))，其中p_i是各类别概率"
    ax_tree.text(0.5, -0.08, entropy_formula, 
                horizontalalignment='center', 
                verticalalignment='top', 
                transform=ax_tree.transAxes,
                fontsize=9, style='italic', color='#666666', usetex=False)
    
    # 在右侧子图添加节点信息解释
    ax_info.axis('off')
    ax_info.set_title('决策树节点信息详解', fontsize=15, pad=15)
    
    # 非叶子节点信息详细解释
    non_leaf_explanation = """
    非叶子节点包含以下详细信息：
    
    1. 特征名称 (feature_name):
       - 用于进行分裂决策的特征属性名称
       - 例如：颜色、形状、大小、重量等
       - 是树选择的最优分裂特征
    
    2. 分裂条件 (feature <= threshold):
       - 特征值的分裂阈值和比较操作符
       - <= 表示如果样本特征值小于等于阈值，进入左子树
       - > 表示如果样本特征值大于阈值，进入右子树
       - 阈值是特征的具体数值（对连续型特征）或编码值（对离散型特征）
    
    3. 熵值 (entropy):
       - 衡量节点样本分布混乱程度的指标
       - 计算公式：-Σ(p_i * log2(p_i))，其中p_i是各类别概率
       - 取值范围：0（完全纯净）到log2(n_classes)（完全混乱）
       - 熵值越低，表示节点纯度越高，分类越确定
       - 树算法选择使熵减最大的分裂点
    
    4. 样本数 (samples):
       - 该节点包含的总样本数量
       - 表示有多少训练样本经过该节点
       - 样本数从根节点到叶节点逐渐减少
    
    非叶子节点是决策树的内部节点，负责根据特征值将样本分配到不同的分支路径。
    每个非叶子节点都包含一个测试条件，决定样本的流向。
    """
    
    # 叶子节点信息详细解释
    leaf_explanation = """
    叶子节点包含以下详细信息：
    
    1. 类别名称 (class):
       - 节点的最终预测类别
       - 例如：苹果、香蕉、柠檬、草莓等
       - 基于节点中占多数的样本类别决定
    
    2. 熵值 (entropy):
       - 衡量叶节点纯度的指标
       - 理想情况下，纯叶节点的熵值为0
       - 熵值越高，表示节点中样本类别分布越均匀
    
    3. 样本分布 (value):
       - 各分类在该叶节点中的具体样本数量
       - 格式为：[类别1数量, 类别2数量, ..., 类别n数量]
       - 对应于class_names中的类别顺序
       - 样本数量最多的类别即为预测类别
    
    4. 总样本数 (samples):
       - 到达并停留在该叶节点的总样本数
       - 反映了该叶节点的代表性和可靠性
    
    叶子节点是决策树的终端节点，不再进行分裂。当一个测试样本沿着树的路径到达
    叶子节点时，该节点的类别即为模型对样本的最终预测结果。
    """
    
    # 决策树颜色含义说明
    color_explanation = """
    颜色含义：
    - 节点颜色深浅表示该节点的纯度（熵值大小）
    - 颜色越深：熵值越小，节点纯度越高
    - 颜色越浅：熵值越大，节点纯度越低
    - 不同颜色区分不同的预测类别（在叶节点中更明显）
    """
    
    # 绘制解释文本
    y_position = 0.97
    ax_info.text(0.05, y_position, '决策树节点数据详解', fontsize=14, fontweight='bold')
    
    y_position -= 0.04
    ax_info.text(0.05, y_position, '-' * 50, fontsize=10)
    
    y_position -= 0.03
    ax_info.text(0.05, y_position, '非叶子节点详解:', fontsize=12, fontweight='bold', color='#0066CC')
    
    y_position -= 0.02
    for line in non_leaf_explanation.strip().split('\n'):
        y_position -= 0.022
        if line.strip().startswith(('1.', '2.', '3.', '4.')):
            ax_info.text(0.05, y_position, line.strip(), fontsize=10, fontweight='bold', wrap=True)
        elif line.strip().startswith(('-', '例如：', '格式为：')):
            ax_info.text(0.08, y_position, line.strip(), fontsize=9, style='italic', wrap=True)
        else:
            ax_info.text(0.05, y_position, line.strip(), fontsize=9, wrap=True)
    
    y_position -= 0.05
    ax_info.text(0.05, y_position, '-' * 50, fontsize=10)
    
    y_position -= 0.03
    ax_info.text(0.05, y_position, '叶子节点详解:', fontsize=12, fontweight='bold', color='#009900')
    
    y_position -= 0.02
    for line in leaf_explanation.strip().split('\n'):
        y_position -= 0.022
        if line.strip().startswith(('1.', '2.', '3.', '4.')):
            ax_info.text(0.05, y_position, line.strip(), fontsize=10, fontweight='bold', wrap=True)
        elif line.strip().startswith(('-', '例如：', '格式为：')):
            ax_info.text(0.08, y_position, line.strip(), fontsize=9, style='italic', wrap=True)
        else:
            ax_info.text(0.05, y_position, line.strip(), fontsize=9, wrap=True)
    
    y_position -= 0.05
    ax_info.text(0.05, y_position, '-' * 50, fontsize=10)
    
    y_position -= 0.03
    ax_info.text(0.05, y_position, '颜色编码说明:', fontsize=11, fontweight='bold', color='#993300')
    
    y_position -= 0.02
    for line in color_explanation.strip().split('\n'):
        y_position -= 0.02
        ax_info.text(0.05, y_position, line.strip(), fontsize=9, wrap=True)
    
    plt.tight_layout()
    return fig

# Main Program
def main():
    # 创建数据集
    df = create_fruit_dataset()
    print("原始数据集：")
    print(df)
    print("\n" + "-"*30)
    
    # 预处理数据
    df_encoded, label_encoders = preprocess_data(df)
    print("编码后的数据集：")
    print(df_encoded)
    print("\n" + "-"*30)
    
    # 分离特征和标签
    feature_names = ['颜色', '形状', '大小', '重量']
    X = df_encoded[feature_names]
    y = df_encoded['类型']
    
    # 分为训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
    
    # 逐步训练不同深度的决策树，展示训练过程
    max_depth = 6  # 修改为6，生成6棵决策树
    print(f"开始逐步训练过程，从深度1到深度{max_depth}...\n")
    
    # 存储每个深度的模型和准确率
    models = []
    accuracies = []
    
    class_names = label_encoders['类型'].classes_
    
    # 创建第一个窗口：显示原始数据集
    dataset_fig = show_dataset_window(df)
    
    # 创建6个窗口：显示不同深度的决策树
    tree_figures = []
    
    for depth in range(1, max_depth + 1):
        print(f"{'='*60}")
        print(f"训练深度为 {depth} 的决策树模型...")
        print(f"{'='*60}")
        
        # 训练当前深度的模型
        model = train_model(X_train, y_train, max_depth=depth)
        models.append(model)
        
        # 在测试集上评估模型
        y_pred = model.predict(X_test)
        accuracy = accuracy_score(y_test, y_pred)
        accuracies.append(accuracy)
        print(f"深度 {depth} 的模型准确率：{accuracy:.2f}")
        
        # 显示当前深度的决策树
        print(f"\n深度 {depth} 的决策树树形结构：")
        print("-"*60)
        print_tree_structure(model, feature_names, class_names)
        print("-"*60)
        
        # 阐述决策树
        explain_decision_tree(model, feature_names, class_names, depth)
        print("\n")
        
        # 创建决策树窗口
        tree_fig = show_decision_tree_window(model, depth, feature_names, class_names)
        tree_figures.append(tree_fig)
        
    # 显示最终训练结果摘要
    print(f"{'='*60}")
    print("训练过程完成！")
    print(f"深度与准确率对比：")
    for i, (depth, acc) in enumerate(zip(range(1, max_depth + 1), accuracies)):
        print(f"深度 {depth}: {acc:.2f}")
    print(f"{'='*60}")
    
    # 使用最终深度的模型
    final_model = models[-1]
    
    # 为两个代表性样本创建执行轨迹窗口
    print(f"\n{'='*60}")
    print("创建决策树执行轨迹可视化...")
    print(f"{'='*60}")
    
    # 选择两个代表性样本进行演示
    demo_samples = [
        {'颜色': '红色', '形状': '圆形', '大小': '中等', '重量': '中等'},  # 苹果样本
        {'颜色': '黄色', '形状': '长形', '大小': '中等', '重量': '中等'}   # 香蕉样本
    ]
    
    # 为每个样本创建执行轨迹窗口
    path_figures = []
    for i, sample in enumerate(demo_samples, 1):
        print(f"\n创建样本 {i} 的决策树执行轨迹窗口...")
        print(f"样本特征: {sample}")
        path_fig = show_decision_path_window(final_model, sample, feature_names, class_names, label_encoders)
        path_figures.append(path_fig)
    
    # 显示所有窗口
    plt.show()
    
    # 测试新样本
    test_fruits = [
        {'颜色': '红色', '形状': '圆形', '大小': '中等', '重量': '中等'},  # 苹果
        {'颜色': '黄色', '形状': '长形', '大小': '中等', '重量': '中等'},  # 香蕉
        {'颜色': '红色', '形状': '圆形', '大小': '大型', '重量': '重'},  # 苹果（替换橙子）
        {'颜色': '黄色', '形状': '椭圆形', '大小': '小型', '重量': '轻'},  # 柠檬（替换西瓜）
        {'颜色': '黄色', '形状': '圆形', '大小': '小型', '重量': '轻'},       # 柠檬
        {'颜色': '红色', '形状': '心形', '大小': '小型', '重量': '轻'}        # 草莓
    ]
    
    # 预测每个测试样本
    print(f"\n{'='*60}")
    print("使用最终模型进行预测...")
    print(f"{'='*60}")
    
    for i, fruit in enumerate(test_fruits, 1):
        print(f"\n\n测试样本 {i}：{fruit}")
        predicted = predict_and_explain(final_model, fruit, label_encoders, feature_names)
        print(f"预测结果：{predicted}")

def show_decision_path_window(model, sample, feature_names, class_names, label_encoders):
    """显示样本在决策树上的执行轨迹"""
    # 编码样本
    sample_encoded = []
    for feature in feature_names:
        encoded_value = label_encoders[feature].transform([sample[feature]])[0]
        sample_encoded.append(encoded_value)
    
    # 获取预测结果和决策路径
    prediction = model.predict([sample_encoded])[0]
    predicted_class = label_encoders['类型'].inverse_transform([prediction])[0]
    
    # 获取决策路径信息
    node_indicator = model.decision_path([sample_encoded])
    leaf_id = model.apply([sample_encoded])[0]
    node_index = node_indicator.indices[node_indicator.indptr[0]:node_indicator.indptr[1]]
    
    # 创建图形和子图
    fig, (ax_tree, ax_info) = plt.subplots(1, 2, figsize=(15, 8), gridspec_kw={'width_ratios': [1, 1]})
    
    # 设置窗口标题
    fig.canvas.manager.set_window_title(f'决策树执行轨迹 - 样本: {sample}')
    
    # 绘制决策树，不使用默认颜色
    plot_tree(model, feature_names=feature_names, class_names=class_names, 
              filled=True, rounded=True, ax=ax_tree, fontsize=10)
    
    # 添加轨迹指示（通过文本注释）
    ax_tree.set_title('决策树执行轨迹（高亮显示）', fontsize=15, pad=20)
    
    # 在决策树下方添加样本信息
    sample_info = f'样本信息: {sample}'
    ax_tree.text(0.5, -0.03, sample_info, 
                horizontalalignment='center', 
                verticalalignment='top', 
                transform=ax_tree.transAxes,
                fontsize=10, fontweight='bold', color='#333333')
    
    # 添加预测结果
    prediction_info = f'预测结果: {predicted_class}'
    ax_tree.text(0.5, -0.08, prediction_info, 
                horizontalalignment='center', 
                verticalalignment='top', 
                transform=ax_tree.transAxes,
                fontsize=10, style='italic', color='#666666')
    
    # 在右侧子图添加决策路径详细解释
    ax_info.axis('off')
    ax_info.set_title('决策路径详细步骤', fontsize=15, pad=15)
    
    # 添加样本信息
    y_position = 0.95
    ax_info.text(0.05, y_position, '样本特征:', fontsize=11, fontweight='bold', color='#993300')
    y_position -= 0.03
    for feature, value in sample.items():
        y_position -= 0.02
        ax_info.text(0.05, y_position, f'  - {feature}: {value}', fontsize=9)
    
    y_position -= 0.05
    ax_info.text(0.05, y_position, '-' * 50, fontsize=10)
    
    # 添加决策路径步骤
    y_position -= 0.03
    ax_info.text(0.05, y_position, '决策路径步骤:', fontsize=11, fontweight='bold', color='#993300')
    
    for i, node_id in enumerate(node_index):
        y_position -= 0.03
        if node_id != leaf_id:
            feature_idx = model.tree_.feature[node_id]
            feature_name = feature_names[feature_idx]
            threshold = model.tree_.threshold[node_id]
            
            original_value = sample[feature_name]
            encoded_value = sample_encoded[feature_idx]
            
            if encoded_value <= threshold:
                direction = "≤"
                branch = "左子树"
            else:
                direction = ">"
                branch = "右子树"
            
            ax_info.text(0.05, y_position, f'步骤 {i+1}:', fontsize=10, fontweight='bold', color='#0066CC')
            y_position -= 0.02
            ax_info.text(0.05, y_position, f'  检查特征: {feature_name} = {original_value} (编码: {encoded_value})', fontsize=9)
            y_position -= 0.02
            ax_info.text(0.05, y_position, f'  条件: {encoded_value} {direction} {threshold:.2f}', fontsize=9)
            y_position -= 0.02
            ax_info.text(0.05, y_position, f'  走向: {branch}', fontsize=9)
        else:
            ax_info.text(0.05, y_position, f'步骤 {i+1}:', fontsize=10, fontweight='bold', color='#0066CC')
            y_position -= 0.02
            ax_info.text(0.05, y_position, f'  到达叶节点，最终预测: {predicted_class}', fontsize=9, color='#CC3300')
    
    plt.tight_layout()
    return fig

# Run main program
if __name__ == "__main__":
    main()